/**
 * Copyright 2009 cestum.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.cestum.cave.presenter.wave
{
	import com.google.protobuf.Message;
	
	import org.goverla.collections.ArrayList;
	import org.goverla.collections.HashMap;
	import org.waveprotocol.wave.model.document.operation.BufferedDocOp;
	import org.waveprotocol.wave.model.document.operation.DocumentOperation;
	import org.waveprotocol.wave.model.document.operation.impl.AnnotationBoundaryMapImpl;
	import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
	import org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl;
	import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder;
	import org.waveprotocol.wave.model.document.operation.impl.util.ArrBuilder;
	import org.waveprotocol.wave.model.operation.wave.AddParticipant;
	import org.waveprotocol.wave.model.operation.wave.NoOp;
	import org.waveprotocol.wave.model.operation.wave.RemoveParticipant;
	import org.waveprotocol.wave.model.operation.wave.WaveletDelta;
	import org.waveprotocol.wave.model.operation.wave.WaveletDocumentOperation;
	import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
	import org.waveprotocol.wave.model.util.Pair;
	import org.waveprotocol.wave.model.wave.ParticipantId;
	import org.waveprotocol.wave.protocol.AnnotationBoundary;
	import org.waveprotocol.wave.protocol.Component;
	import org.waveprotocol.wave.protocol.KeyValuePair;
	import org.waveprotocol.wave.protocol.KeyValueUpdate;
	import org.waveprotocol.wave.protocol.MutateDocument;
	import org.waveprotocol.wave.protocol.MutateDocumentBuilder;
	import org.waveprotocol.wave.protocol.ProtocolDocumentOperation;
	import org.waveprotocol.wave.protocol.ProtocolDocumentOperationBuilder;
	import org.waveprotocol.wave.protocol.ProtocolHashedVersion;
	import org.waveprotocol.wave.protocol.ProtocolWaveletDelta;
	import org.waveprotocol.wave.protocol.ProtocolWaveletDeltaBuiler;
	import org.waveprotocol.wave.protocol.ProtocolWaveletOperation;
	import org.waveprotocol.wave.protocol.ProtocolWaveletOperationBuilder;
	
	public class WaveletOperationSerializer
	{
		public function WaveletOperationSerializer()
		{
		}
		/**
		 * Overloaded serialize method
		 **/
		public static function serialize(op:Object, version:HashedVersion =null):Message {
			if (op instanceof WaveletOperation)
				return serializeWaveletOp(op as WaveletOperation);
			else if (op is DocumentOperation)
				return serializeDocOp(op as DocumentOperation);
			else if (op instanceof HashedVersion)
				return serializeHashedVersion(op as HashedVersion);
			else if (op instanceof WaveletDelta)
				return serializeWaveletDelta(op as WaveletDelta, version);
			return null;
		}
		/**
		 * Overloaded serialize method
		 **/
		public static function deserialize(op:Object):Object {
			if (op instanceof ProtocolWaveletDelta)
				return deserializeWaveletDelta(op as ProtocolWaveletDelta);
			else if (op instanceof ProtocolDocumentOperation)
				return deserializeDocOp(op as ProtocolDocumentOperation);
			else if (op instanceof ProtocolHashedVersion)
				return deserializeHashedVersion(op as ProtocolHashedVersion);
			else if (op instanceof ProtocolWaveletOperation)
				return deserializeWaveletOperation(op as ProtocolWaveletOperation);		
			return null;
		}
		/**
		 * Serialize a {@link WaveletDelta} as a {@link ProtocolWaveletDelta} at a
		 * specific version.
		 *
		 * @param waveletDelta to serialize
		 * @param version version at which the delta applies
		 * @return serialized protocol buffer wavelet delta
		 */
		public static function serializeWaveletDelta(waveletDelta:WaveletDelta, version:HashedVersion):ProtocolWaveletDelta {
			var protobufDelta:ProtocolWaveletDeltaBuiler = ProtocolWaveletDelta.newBuilder();
			
			var size:int = waveletDelta.getOperations().length;
			for (var i:int = 0 ;i< size; i++) {
				var waveletOp:WaveletOperation =  waveletDelta.getOperations().getItemAt(i) as WaveletOperation;
				protobufDelta.addOperation(serialize(waveletOp) as ProtocolWaveletOperation);
			}
			
			protobufDelta.setAuthor(waveletDelta.getAuthor().getAddress());
			protobufDelta.setHashedVersion(serialize(version) as ProtocolHashedVersion);
			return ProtocolWaveletDelta(protobufDelta.build());
		}
		

		/**
		 * Serialize a {@link WaveletOperation} as a {@link ProtocolWaveletOperation}.
		 *
		 * @param waveletOp wavelet operation to serialize
		 * @return serialized protocol buffer wavelet operation
		 */
		public static function serializeWaveletOp(waveletOp:WaveletOperation):ProtocolWaveletOperation {
			var protobufOp:ProtocolWaveletOperationBuilder= ProtocolWaveletOperation.newBuilder();
			
			if (waveletOp instanceof NoOp) {
				protobufOp.setNoOp(true);
			} else if (waveletOp instanceof AddParticipant) {
				protobufOp.setAddParticipant(
					(AddParticipant(waveletOp)).getParticipantId().getAddress());
			} else if (waveletOp instanceof RemoveParticipant) {
				protobufOp.setRemoveParticipant(
					(RemoveParticipant(waveletOp)).getParticipantId().getAddress());
			} else if (waveletOp instanceof WaveletDocumentOperation) {
				var mutation:MutateDocumentBuilder= MutateDocument.newBuilder();
				mutation.setDocumentId((WaveletDocumentOperation(waveletOp)).getDocumentId());
				mutation.setDocumentOperation(ProtocolDocumentOperation(serialize((WaveletDocumentOperation(waveletOp)).getOperation())));
				protobufOp.setMutateDocument(MutateDocument(mutation.build()));
			} else {
				throw new Error("Unsupported operation type: " + waveletOp);
			}
			
			return ProtocolWaveletOperation(protobufOp.build());
		}
		
		/**
		 * Serialize a {@link DocOp} as a {@link ProtocolDocumentOperation}.
		 *
		 * @param inputOp document operation to serialize
		 * @return serialized protocol buffer document operation
		 */
		public static function serializeDocOp(inputOp:DocumentOperation):ProtocolDocumentOperation {
			var output:ProtocolDocumentOperationBuilder= ProtocolDocumentOperation.newBuilder();
			inputOp.apply(new InputOpDocCursor(output));
			return ProtocolDocumentOperation(output.build());
		}
		
		/**
		 * Deserializes a {@link ProtocolWaveletDelta} as a {@link WaveletDelta} and
		 * {@link HashedVersion}.
		 *
		 * @param delta protocol buffer wavelet delta to deserialize
		 * @return deserialized wavelet delta and version
		 */
		public static function deserializeWaveletDelta(delta:ProtocolWaveletDelta):Pair {
			var ops:ArrayList = new ArrayList();
			var size:int = delta.operation_.length;
			for (var i:int=0; i< size; i++) {
				var op:ProtocolWaveletOperation = delta.operation_.getItemAt(i) as ProtocolWaveletOperation;
				ops.addItem(deserialize(op) as WaveletOperation);
			}
			
			var hashedVersion:HashedVersion= deserialize(delta.hashedVersion_) as HashedVersion;
			return Pair.of(new WaveletDelta(new ParticipantId(delta.author_), ops), hashedVersion);
		}
		
		/** Deserializes a protobuf to a HashedVersion POJO. */
		public static function deserializeHashedVersion(hashedVersion:ProtocolHashedVersion):HashedVersion {
			return new HashedVersion(hashedVersion.version_,hashedVersion.historyHash_);
		}
		
		/** Serializes a HashedVersion POJO to a protobuf. */
		public static function serializeHashedVersion(hashedVersion:HashedVersion):ProtocolHashedVersion {
			return ProtocolHashedVersion.newBuilder().setVersion(
				hashedVersion.getVersion()).setHistoryHash(hashedVersion.getHistoryHash()).build() as ProtocolHashedVersion;
		}
		
		/**
		 * Deserialize a {@link ProtocolWaveletOperation} as a {@link WaveletOperation}.
		 *
		 * @param protobufOp protocol buffer wavelet operation to deserialize
		 * @return deserialized wavelet operation
		 */
		public static function deserializeWaveletOperation(protobufOp:ProtocolWaveletOperation):WaveletOperation {
			if (protobufOp.hasNoOp) {
				return new NoOp();
			} else if (protobufOp.hasAddParticipant) {
				return new AddParticipant(new ParticipantId(protobufOp.addParticipant));
			} else if (protobufOp.hasRemoveParticipant) {
				return new RemoveParticipant(new ParticipantId(protobufOp.removeParticipant));
			} else if (protobufOp.hasMutateDocument) {
				return new WaveletDocumentOperation(
					protobufOp.mutateDocument.documentId_,
					BufferedDocOp(deserialize(protobufOp.mutateDocument.documentOperation_)));
			} else {
				throw new Error("Unsupported operation: " + protobufOp);
			}
		}
		
		/**
		 * Deserialize a {@link ProtocolDocumentOperation} into a {@link DocOp}.
		 *
		 * @param op protocol buffer document operation to deserialize
		 * @return deserialized DocOp
		 */
		public static function deserializeDocOp(op:ProtocolDocumentOperation):BufferedDocOp {
			var output:DocOpBuilder= new DocOpBuilder();
			var size:int = op.component_.length;
			for (var i:int = 0 ;i< size; i++) {
				var c:Component =  op.component_.getItemAt(i) as Component;
				if (c.hasAnnotationBoundary) {
					var ann:AnnotationBoundary = c.annotationBoundary_;
					if (ann.empty_) {
						output.annotationBoundary(new ArrBuilder().build());
					} else {
						var ends:Array = new Array(ann.end_.length);
						var changeKeys:Array = new Array(ann.change_.length);
						var oldValues:Array = new Array(ann.change_.length);
						var newValues:Array = new Array(ann.change_.length);
						if (ann.end_.length > 0) {
							ends = ann.end_.toArray();
						}
						for (var annI:int = 0; annI < changeKeys.length; annI++) {
							var kvu:KeyValueUpdate = ann.change_.getItemAt(annI) as KeyValueUpdate;
							changeKeys[annI] = kvu.key_;
							oldValues[annI] = kvu.hasOldValue ? kvu.oldValue_ : null;
							newValues[annI] = kvu.hasNewValue ? kvu.newValue_ : null;
						}
						output.annotationBoundary(
							new AnnotationBoundaryMapImpl(ends, changeKeys, oldValues, newValues));
					}
				} else if (c.hasCharacters) {
					output.characters(c.characters_);
				} else if (c.hasElementStart) {
					var attributesMap:HashMap = new HashMap();
					var size1:int = c.elementStart_.attribute_.length;
					for (var i1:int = 0 ;i1< size1; i1++) {
						var pair:KeyValuePair =	c.elementStart_.attribute_.getItemAt(i1) as KeyValuePair;
						attributesMap.addItem(pair.key_, pair.value_);
					}
					output.elementStart(c.elementStart_.type_, AttributesImpl.of(attributesMap));
				} else if (c.hasElementEnd) {
					output.elementEnd();
				} else if (c.hasRetainItemCount) {
					output.retain(c.retainItemCount_);
				} else if (c.hasDeleteCharacters) {
					output.deleteCharacters(c.deleteCharacters_);
				} else if (c.hasDeleteElementStart) {
					var attributesMap:HashMap = new HashMap();
					var size2:int = c.deleteElementStart_.attribute_.length;
					for (var i3:int = 0 ;i3< size2; i3++) {
						var pair:KeyValuePair =	c.deleteElementStart_.attribute_.getItemAt(i3) as KeyValuePair;
						attributesMap.addItem(pair.key_, pair.value_);
					}
					output.deleteElementStart(c.deleteElementStart_.type_, AttributesImpl.of(attributesMap));
				} else if (c.hasDeleteElementEnd) {
					output.deleteElementEnd();
				} else if (c.hasReplaceAttributes) {
					if (c.replaceAttributes_.empty_) {
						//think?
						output.replaceAttributes(new AttributesImpl(null), new AttributesImpl(null));
					} else {
						var oldAttributesMap:HashMap = new HashMap();
						var newAttributesMap:HashMap = new HashMap();
						var replaceSize1:int = c.replaceAttributes_.oldAttribute_.length;
						for (var replaceI:int = 0 ;replaceI< replaceSize1; replaceI++) {
							var pair:KeyValuePair = c.replaceAttributes_.oldAttribute_.getItemAt(replaceI) as KeyValuePair;
							oldAttributesMap.addItem(pair.key_, pair.value_);
						}
						var replaceSize2:int = c.replaceAttributes_.newAttribute_.length;
						for (var replace2:int = 0 ;replace2< replaceSize2; replace2++) {
							var pair1:KeyValuePair = c.replaceAttributes_.newAttribute_.getItemAt(replace2) as KeyValuePair;
							newAttributesMap.addItem(pair1.key_, pair1.value_);
						}
						output.replaceAttributes(AttributesImpl.of(oldAttributesMap),	AttributesImpl.of(newAttributesMap));
					}
				} else if (c.hasUpdateAttributes) {
					if (c.updateAttributes_.empty_) {
						output.updateAttributes(AttributesUpdateImpl.EMPTY_MAP);
					} else {
						var upLength:int= c.updateAttributes_.attributeUpdate_.length;
						var triplets:Array = new Array(upLength * 3);
						for (var upI:int = 0, upJ = 0; upI < upLength; upI++) {
							var  kvu:KeyValueUpdate =	c.updateAttributes_.attributeUpdate_.getItemAt(upI) as KeyValueUpdate;
							triplets[upJ++] = kvu.key_;
							triplets[upJ++] = kvu.hasOldValue ? kvu.oldValue_ : null;
							triplets[upJ++] = kvu.hasNewValue ? kvu.newValue_ : null;
						}
						output.updateAttributes(AttributesUpdateImpl.ofArray(triplets));
					}
				} else {
					//throw new Error("Unsupported operation component: " + c);
				}
			}
			
			return output.build();
		}
}
}